Skip to main content

Prover Return Decoding Guide

info

This guide explains how to interpret and decode the returns from the CrossL2ProverV2 contract, using a practical multi-rollup example to demonstrate the complete process.

Overview

The setValueFromSource function demonstrates how to:

  • Take a proof from the Prove API
  • Validate cross-chain events using the prover contract
  • Decode and process the returned data securely

This process involves several critical steps of data validation and decoding that ensure cross-chain security.

Understanding the Interface

CrossL2Prover validateEvent Function

CrossL2Prover Interface
function validateEvent(bytes calldata proof)
returns (
uint32 chainId, // Source chain identifier
address emittingContract, // Emitting contract address
bytes topics, // Concatenated Event topics
bytes unindexedData // Non-indexed event parameters
)

← Return Values

Four key pieces of validated data from the cross-chain event

🗄️ Data Format

Topics as concatenated bytes, unindexed data as ABI-encoded parameters

Raw Return Data Structure

Raw Return Example
[
11155420, // uint32: chainId (Optimism Sepolia)
"0x24B1D355f5B254aF86860bBe4214aEDe2DB1314E", // address: emittingContract
"0x...", // bytes: topic1 + topic2 + topic3
"0x..." // bytes: ABI encoded non-indexed parameters
]

Event Structure Reference

Understanding the origin event structure is crucial for proper decoding:

Origin Event Structure
event ValueSet(
// → topic[0] = keccak(ValueSet(address,string,bytes,uint256,bytes32,uint256))
address indexed sender, // In topics[1]
string key, // In unindexedData
bytes value, // In unindexedData
uint256 nonce, // In unindexedData
bytes32 indexed hashedKey, // In topics[2]
uint256 version // In unindexedData
);
info

Topic Distribution: topics[0] contains the event signature hash, topics[1-2] contain indexed parameters, and non-indexed parameters are ABI-encoded in unindexedData.

Step-by-Step Decoding & Validation Process

1. Initial Proof Validation

First, validate the proof and extract the basic return values:

Proof Validation
(
uint32 sourceChainId,
address sourceContract,
bytes memory topics,
bytes memory unindexedData
) = polymerProver.validateEvent(proof);

What you get:

  • sourceChainId: Origin chain identifier
  • sourceContract: Contract that emitted the event
  • topics: Concatenated event topics (requires parsing)
  • unindexedData: ABI-encoded parameters (requires decoding)

2. Source Chain ID Validation

Validate that the event originated from an expected source chain:

Source Chain Validation
require(allowedSourceChains[sourceChainId], "Invalid source chain");
warning

Critical Security Check: This ensures the event came from an authorized chain. If skipped, an event emitted on a different chain can be used to spoof the intended chain.

3. Source Emitting Contract Validation

Validate that the event was emitted by the expected contract:

if(sourceContract != expectedSourceContract){
revert invalidEmittingAddress();
}
)c;
warning

Critical Security Check: This ensures that the event originated from a trusted contract and not from an arbitrary contract on the source chain that can be deployed by anyone. If skipped, a malicious contract on an allowed source chain can spoof events with arbitrary data.

4. Topics Length Validation

The topics length must be checked against the expected length.

require(
topics_from_proof.length == EXPECTED_LENGTH,
"Topics length does not match expected length"
);
warning

Critical Security Check: This validates that the topics length matches what is expected for the event being decoded. If this validation is skipped, arbitrary length topic arrays can be passed in and impact parsing of unindexed data.

5. Event Signature Validation

Parse the topics bytes into individual bytes32 values and verify the event signature:

Topics Parsing & Signature Verification
// Parse topics into bytes32 array
bytes32[] memory topicsArray = new bytes32[](3);
require(topics.length == 96, "Invalid topics length"); // 3 * 32 bytes

assembly {
let topicsPtr := add(topics, 32) // Skip length prefix
for { let i := 0 } lt(i, 3) { i := add(i, 1) } {
mstore(
add(add(topicsArray, 32), mul(i, 32)),
mload(add(topicsPtr, mul(i, 32)))
)
}
}

// Verify event signature
bytes32 expectedSelector = keccak256("ValueSet(address,string,bytes,uint256,bytes32,uint256)");
require(topicsArray[0] == expectedSelector, "Invalid event signature");
warning

Critical Security Check: This ensures only ValueSet events are processed and prevents attacks from similar events with different parameter types. If skipped, events with different parameter types incorrectly processed, leading to corrupted event data.

6. Topics Array — Indexed Data Extraction

Using the topicsArray parsed in the previous step, extract the indexed parameters:

Indexed Parameters
address sender = address(uint160(uint256(topicsArray[1])));
bytes32 hashedKey = topicsArray[2];

Result:

  • topicsArray[0]: Event signature hash (validated in step 5)
  • topicsArray[1]: Indexed sender address (padded to 32 bytes)
  • topicsArray[2]: Indexed hashedKey

Conversion Logic:

  • sender: bytes32uint256uint160address
  • hashedKey: Direct use (already bytes32)
warning

Important: If skipped, indexed event parameters won't be available for processing.

7. Decode Non-Indexed Event Data

Extract the remaining parameters from the ABI-encoded data:

Non-Indexed Decoding
(
, // skip key (we use hashedKey from topics)
bytes memory value,
uint256 nonce,
uint256 version
) = abi.decode(
unindexedData,
(string, bytes, uint256, uint256)
);

Parameters Retrieved:

  • key: Skipped (using hashedKey from topics instead)
  • value: The actual data to store
  • nonce: For replay protection
  • version: For version control
warning

Important: If skipped, non-indexed event parameters won't be available for processing.

8. Implement Replay Protection at App Level

Prevent the same event from being processed multiple times:

Replay Protection
// Create and verify unique proof hash for replay protection
bytes32 uniqueHash = keccak256(
abi.encodePacked(sourceChainId, sourceContract, hashedKey, nonce)
);
require(!usedUniqueHashes[uniqueHash], "hashKey already used");
usedUniqueHashes[uniqueHash] = true;
warning

Critical Security Check: Applications should implement replay protection using data from their event directly to prevent duplicate processing of the same cross-chain event. If skipped, the same cross-chain event could be processed infinitely many times, leading to double-spending or duplicate state changes.

Common Pitfalls & Solutions

warning

Critical Mistake: The validateEvent function returns event topics as a single bytes array, NOT a bytes32[] array.

❌ Incorrect Approach

Wrong Implementation
( , , bytes memory topics, ) = crossL2Prover.validateEvent(proof);
bytes32 eventSig = bytes32(topics[0]); // ❌ Reads first BYTE, not first TOPIC

✅ Correct Implementation

Correct Implementation
// Example for an event with 3 topics
require(topics.length == 3 * 32, "Invalid topics length");

// 1. Create a memory array
bytes32[] memory topicsArray = new bytes32[](3);

// 2. Use assembly to parse the topics
assembly {
let topicsPtr := add(topics, 32) // Skip bytes length
mstore(add(topicsArray, 32), mload(topicsPtr))
mstore(add(topicsArray, 64), mload(add(topicsPtr, 32)))
mstore(add(topicsArray, 96), mload(add(topicsPtr, 64)))
}

// 3. ✅ Use the new array for your logic
bytes32 eventSig = topicsArray[0];
tip

Rule of Thumb: Always parse the topics bytes variable into a bytes32[] array before use.

Production Security Checklist

warning

Important: Proof for a given event is not unique and should not be used as a unique identifier for replay protection.

1. Source Chain Validation

Prevent event duplication from unauthorized chains:

Source Chain Security
mapping(uint32 => bool) public allowedSourceChains;
require(allowedSourceChains[sourceChainId], "Invalid source chain");

Purpose: Safeguards against event duplication from different chains.

Complete Implementation Example

Full Implementation
function setValueFromSource(bytes calldata proof) external {
// 1. Validate the proof
(
uint32 sourceChainId,
address sourceContract,
bytes memory topics,
bytes memory unindexedData
) = polymerProver.validateEvent(proof);

// 2. Source chain ID validation
require(allowedSourceChains[sourceChainId], "Invalid source chain");

// 3. Source emitting contract validation
require(authorizedContracts[sourceChainId][sourceContract], "Unauthorized contract");

// 4. Topics length validation
require(topics.length == 96, "Invalid topics length");

// 5. Event signature validation (includes topics parsing)
bytes32[] memory topicsArray = new bytes32[](3);

assembly {
let topicsPtr := add(topics, 32)
mstore(add(topicsArray, 32), mload(topicsPtr))
mstore(add(topicsArray, 64), mload(add(topicsPtr, 32)))
mstore(add(topicsArray, 96), mload(add(topicsPtr, 64)))
}

bytes32 expectedSelector = keccak256("ValueSet(address,string,bytes,uint256,bytes32,uint256)");
require(topicsArray[0] == expectedSelector, "Invalid event signature");

// 6. Extract indexed parameters from topics array
address sender = address(uint160(uint256(topicsArray[1])));
bytes32 hashedKey = topicsArray[2];

// 7. Decode non-indexed event data
(
, // skip key
bytes memory value,
uint256 nonce,
uint256 version
) = abi.decode(unindexedData, (string, bytes, uint256, uint256));

// 8. Implement replay protection at app level
bytes32 uniqueHash = keccak256(
abi.encodePacked(sourceChainId, sourceContract, hashedKey, nonce)
);
require(!usedUniqueHashes[uniqueHash], "hashKey already used");
usedUniqueHashes[uniqueHash] = true;

// Process the validated data
_processValidatedData(sender, hashedKey, value, nonce, version);
}
info

Security First: This example includes all critical security validations for production use, ensuring robust cross-chain event processing.